package org.ribizli.games.amoeba;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Application;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.AsyncTask;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Log;


public class BackgroudService implements OnSharedPreferenceChangeListener {
  private final static String TAG = "Amoeba-BGService";
  private BGServiceListener mListener;
  private String mServerURL;
  //private Context mContext;
  private String mToken;
  @SuppressWarnings("unchecked")
	private AsyncTask mCurrTask;
  private ApplicationEx mApp;
  private List<JSONObject> mMessageQueue;
	private boolean mLogoutRequested = false;
	private Handler mHandler;
	private long mPollTimeout;
  private long mStatisticsTimeout;
	private boolean mPolling = false;
	private boolean mInProgress = false;
  
  private static final String STATUS_OK = "OK";
  private static final String PREF_KEY_SERVICE_URL = "serviceURL";

  private static final String FIELD_ERROR = "error";
  private static final String FIELD_STATUS = "status";
  private static final String FIELD_TOKEN = "token";
  private static final String FIELD_TASK = "task";
  private static final String FIELD_GAMEID = "game_id";
  private static final String FIELD_USERNAME = "username";
  private static final String FIELD_PASSWORD = "password";
  private static final String FIELD_DETAILS = "details";
  private static final String FIELD_MESSAGES = "messages";
  private static final String FIELD_ACTION = "action";
  private static final String FIELD_SCORE = "score";
  private static final String FIELD_X = "x";
  private static final String FIELD_Y = "y";
  private static final String FIELD_FIRST = "first";
  private static final String FIELD_OPPONENT = "opponent";
  private static final String FIELD_WINNER_ROW = "winner_row";
  private static final String FIELD_ONLINE_USERS = "online_users";
  private static final String FIELD_PLAYING_USERS = "playing_users";
  private static final String FIELD_MESSAGE_TEXT = "message_text";

  private static final String TASK_LOGIN = "login";
  private static final String TASK_LOGOUT = "logout";
  private static final String TASK_MESSAGE = "message";
  private static final String TASK_REGISTER = "register";
  
  private static final String ACTION_WAITING_FOR_OPPONENT = "WAITING_FOR_OPPONENT";
  private static final String ACTION_NEW_GAME = "NEW_GAME";
  private static final String ACTION_YOUR_TURN = "YOUR_TURN";
  private static final String ACTION_YOU_WON = "YOU_WON";
  private static final String ACTION_YOU_LOOSE = "YOU_LOOSE";
  private static final String ACTION_OPPONENT_LEFT = "OPPONENT_LEFT";
  private static final String ACTION_USER_STATISTICS = "USER_STATISTICS";
  private static final String ACTION_SEND_MESSAGE = "SEND_MESSAGE";
  private static final String ACTION_INVALID_MOVE = "INVALID_MOVE";
  private static final String ACTION_INVALID_STATE = "INVALID_STATE";
  private static final String ACTION_REMATCH = "REMATCH";
  private static final String ACTION_UPDATE_PROFILE = "UPDATE_PROFILE";
  private static final String ACTION_MAKE_MOVE = "MAKE_MOVE";

  
  public static final String ERROR_GAME_NOT_FOUND = "GAME_NOT_FOUND";
  public static final String ERROR_INVALID_MESSAGE = "INVALID_MESSAGE";
  public static final String ERROR_INVALID_TOKEN = "INVALID_TOKEN";

  public static final String ERROR_USER_NOT_LOGGED = "USER_NOT_LOGGED";
  public static final String ERROR_USER_ALREADY_REGISTERED = "USER_ALREADY_REGISTERED";
  public static final String ERROR_USER_NOT_REGISTERED = "USER_NOT_REGISTERED";
  public static final String ERROR_WRONG_PASSWORD = "WRONG_PASSWORD";

  public static final String ERROR_NETWORK_ERROR = "NETWORK_ERROR";
  
  
  private static final String GAMEID = "org.ribizli.basicgameserver.AmoebaGame";
  
  private Runnable mSendMessagesRunnable = new Runnable() {
		@Override
		public void run() {
		  Log.d(TAG, "SendMessagesRunable.run()");
			sendMessages();
		}
  };

  private Runnable mStatisticsMessagesRunnable = new Runnable() {
		@Override
		public void run() {
      Log.d(TAG, "StatisticsMessagesRunnable.run()");
			try {
				queueMessage(new JSONObject().put(FIELD_ACTION, ACTION_USER_STATISTICS));
			} catch (JSONException e) {
	      Log.e(TAG, e.getMessage(), e);
			}
		}
  };
  
  public BackgroudService(Application app) {
    if (app == null) {
      throw new NullPointerException();
    }
    mApp = (ApplicationEx)app;
    mMessageQueue = new Vector<JSONObject>();
    SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(app);
    pref.registerOnSharedPreferenceChangeListener(this);
    mServerURL = pref.getString(PREF_KEY_SERVICE_URL, null);
    mPollTimeout = Integer.valueOf(getResourceString(R.string.pollingTimeout)) * 1000;
    mStatisticsTimeout = Integer.valueOf(getResourceString(R.string.statisticsTimeout)) * 1000;
    
  }
  
  private String getResourceString(int res) {
  	return mApp.getResources().getString(res);
  }
  
  private boolean inProgress() {
  	if (mInProgress) Log.i(TAG, "Request in progress");
  	return mInProgress;
  }
  
  private void setPolling(boolean p) {
  	Log.d(TAG, "polling: " + p);
  	this.mPolling = p;
  	if (!p && mHandler != null) {
      Log.d(TAG, "remove SendMessagesRunable");
  		mHandler.removeCallbacks(mSendMessagesRunnable);
  	}
  }

  public void login(String username, String password) {
  	if (inProgress()) return;
    LoginTask logTask = new LoginTask();
    Log.d(TAG, "login: " + username);
    logTask.execute(username, password);
    mCurrTask = logTask;
  }
  
  public void register(String userName, String password, Map<String, String> details) {
  	if (inProgress()) return;
  	RegisterTask regTask = new RegisterTask();
  	if (details == null) {
  	  details = new HashMap<String, String>(1);
  	}
 	  if (!details.containsKey("payload")) {
	    details.put("payload", "android");
 	  }
    Log.d(TAG, "register: " + userName);
  	regTask.execute(userName, password, new JSONObject(details).toString());
  	mCurrTask = regTask;
  }
  
  public void logout() {
  	if (this.mToken == null && mListener != null) { 
  		mListener.loggedOut();
  	} else {
	  	if (inProgress()) {
	  		// Trick to wait until the current messages arrive
	  		mLogoutRequested  = true;
	  	} else {
	  		LogoutTask loTask = new LogoutTask();
	      Log.d(TAG, "logout");
	  		loTask.execute();
	  		mCurrTask = loTask;
	  	}
  	}
  }
  
  public void newGame() {
    try {
      queueMessage(new JSONObject().put(FIELD_ACTION, ACTION_NEW_GAME));
    } catch (JSONException e) {
      Log.e(TAG, e.getMessage(), e);
    }
  }

  public void rematch() {
    try {
      queueMessage(new JSONObject().put(FIELD_ACTION, ACTION_REMATCH));
    } catch (JSONException e) {
      Log.e(TAG, e.getMessage(), e);
    }
  }

  public void makeMove(int x, int y) {
    try {
      queueMessage(new JSONObject()
      	.put(FIELD_ACTION, ACTION_MAKE_MOVE)
      	.put(FIELD_X, x)
      	.put(FIELD_Y, y));
    } catch (JSONException e) {
      Log.e(TAG, e.getMessage(), e);
    }
  }

  
  private void queueMessage(JSONObject msg) {
  	synchronized (mMessageQueue) {
    	mMessageQueue.add(msg);
		}
  	// try to send
  	if (mHandler != null) mHandler.removeCallbacks(mSendMessagesRunnable);
  	sendMessages();
  }
  
  private void sendMessages() {
  	if (!inProgress()) {
	    MessageTask msgTask = new MessageTask();
	    msgTask.execute();
	    mCurrTask = msgTask;
  	}
  }
  
  public void setListener(BGServiceListener l) {
    mListener = l;
    // Uses a little trick, since this is called from the UI tread
    if (l != null) {
    	mHandler = new Handler();
    } else {
      // If there is a scheduled messageSend
    	if (mHandler != null) mHandler.removeCallbacks(mSendMessagesRunnable);
    	mHandler = null;
    }
  }
  
  public void cancelCurrent() {
  	// cancel if scheduled
    Log.d(TAG, "Cancel current");
  	if (mHandler != null) {
      Log.d(TAG, "remove all callbacks");
  	  mHandler.removeCallbacks(mSendMessagesRunnable);
  	  mHandler.removeCallbacks(mStatisticsMessagesRunnable);
  	}
    if (mCurrTask != null && !mCurrTask.isCancelled()) {
      mCurrTask.cancel(true);
      mInProgress = false;
      mCurrTask = null;
      Log.d(TAG, "Canceled current");
    }
  }
  
  public boolean isLogged() {
  	return mToken != null;
  }
  
  public static interface BGServiceListener {
    public void registered(String u, String p);
    
    public void loggedIn(String username, int score);
    
    public void loggedOut();
    
    public void waitingForOpponent();
    
    public void newGame(String opponent, int oppScore, boolean youFirst);
    
    public void yourTurn(int oppX, int oppY);
    
    public void youWon(int yourScore, int oppScore, int[][] winnerRow);

    public void youLoose(int x, int y, int yourScore, int oppScore, int[][] winnerRow);
    
    public void opponentLeft();
    
    public void error(String msg);
    
    public void userStatistics(int online, int playing);
    
    public void messageFrom(String opponent, String message);
    
    public void rematch();
    
    public void profileUpdated();
  }
  
  public static abstract class BasicBGServiceListener implements BGServiceListener {
    public void registered(String u, String p) {}
    
    public void loggedIn(String username, int score) {}
    
    public void loggedOut() {}
    
    public void waitingForOpponent() {}
    
    public void newGame(String opponent, int oppScore, boolean youFirst) {}
    
    public void yourTurn(int oppX, int oppY) {}
    
    public void youWon(int yourScore, int oppScore, int[][] winnerRow) {}

    public void youLoose(int x, int y, int yourScore, int oppScore, int[][] winnerRow) {}
    
    public void opponentLeft() {}
    
    public void error(String msg) {}
    
    public void userStatistics(int online, int playing) {}
    
    public void messageFrom(String opponent, String message) {}
    
    public void rematch() {}
    
    public void profileUpdated() {}
  }
  
  private abstract class BasicTask extends AsyncTask<String, Integer, JSONObject> {

    @Override
    protected void onPreExecute() {
      mInProgress = true;
    }

    private JSONObject readJSONFromIS(InputStream is) throws IOException, JSONException {
      StringBuilder sb = new StringBuilder();
      BufferedReader br = new BufferedReader(new InputStreamReader(is), 1024);
      String line;
      //String NL = System.getProperty("line.separator");
      while ((line = br.readLine()) != null) {
        sb.append(line);
      }
      br.close();
      return new JSONObject(sb.toString());
    }
  	
  	protected JSONObject sendRequest(Map<String, String> params) throws GameException {
      HttpClient c = mApp.getHttpClient();
      HttpPost post = new HttpPost(mServerURL);
      
      ArrayList<NameValuePair> atts = new ArrayList<NameValuePair>();
      for (String key : params.keySet()) {
      	atts.add(new BasicNameValuePair(key, params.get(key)));
			}
      InputStream is = null;
      try {
				post.setEntity(new UrlEncodedFormEntity(atts));
				is = c.execute(post).getEntity().getContent();
				return readJSONFromIS(is);
			} catch (Exception e) {
				Log.e(TAG, e.getMessage(), e);
				throw new GameException(ERROR_NETWORK_ERROR, e);
			} finally {
				if (is != null) {
					try {
						is.close();
					} catch (IOException e) {
					}
				}
			}
  	}
    protected void onPostExecute(JSONObject obj) {
      mInProgress = false;
      mCurrTask = null;
    	if (this.isCancelled()) return;
    	handleResult(obj);
    	if (mLogoutRequested) {
    		mLogoutRequested = false;
    		setPolling(false);
    		logout();
    	}
    }

		protected abstract void handleResult(JSONObject obj);
  }
  
  private class LoginTask extends BasicTask {
		@Override
    protected JSONObject doInBackground(String... params) {
      try {
        HashMap<String, String> toPost = new HashMap<String, String>(5);
        toPost.put(FIELD_TASK, TASK_LOGIN);
        toPost.put(FIELD_USERNAME, params[0]);
        toPost.put(FIELD_PASSWORD, params[1]);
        toPost.put(FIELD_GAMEID, GAMEID);
        return sendRequest(toPost);
      } catch (GameException e) {
        Log.e(TAG, "Login Exception", e);
        try {
					return new JSONObject().put(FIELD_ERROR, e.getMessage());
				} catch (JSONException e1) {
				  return null;
				}
      }
    }
    @Override
    protected void handleResult(JSONObject obj) {
    	// do nothing if the task was canceled during the request
      if (obj.has(FIELD_ERROR)) {
    		if (mListener != null)
    			mListener.error(obj.optString(FIELD_ERROR));
      } else if (obj.has(FIELD_TOKEN)) {
    		// login success
      	mToken = obj.optString(FIELD_TOKEN);
        Log.d(TAG, "Login success: " + mToken);
      	JSONObject details = obj.optJSONObject(FIELD_DETAILS);
    		if (mListener != null)
    			mListener.loggedIn(details.optString(FIELD_USERNAME), details.optInt(FIELD_SCORE));
      	mStatisticsMessagesRunnable.run();
      } else {
      	Log.e(TAG, "Invalid login response: " + obj);
      }
    }
  }
  
  private class LogoutTask extends BasicTask {
    @Override
    protected JSONObject doInBackground(String... params) {
      try {
        HashMap<String, String> toPost = new HashMap<String, String>(5);
        toPost.put(FIELD_TASK, TASK_LOGOUT);
        toPost.put(FIELD_TOKEN, mToken);
        return sendRequest(toPost);
      } catch (GameException e) {
        Log.e(TAG, "Logout Exception", e);
        try {
					return new JSONObject().put("error", e.getMessage());
				} catch (JSONException e1) {
          return null;
				}
      }
    }
    @Override
    protected void handleResult(JSONObject obj) {
    	// do nothing if the task was canceled during the request
      if (obj.has(FIELD_ERROR)) {
      	String error = obj.optString(FIELD_ERROR);
      	if (ERROR_INVALID_TOKEN.equals(error)) {
      		mToken = null; // remove token
      		setPolling(false); // also stop polling
      	}
    		if (mListener != null)
    			mListener.error(error);
      } else if (STATUS_OK.equals(obj.optString(FIELD_STATUS)) && TASK_LOGOUT.equals(obj.optString(FIELD_TASK))) {
    		// logout success
        Log.d(TAG, "logout succeeded");
      	mToken = null;
      	setPolling(false);
      	// remove StatisticsMessagesRunnable update too, since setPolling(false) doesn't
      	if (mHandler != null) mHandler.removeCallbacks(mStatisticsMessagesRunnable);
    		if (mListener != null)
    			mListener.loggedOut();
      } else {
      	Log.e(TAG, "Invalid logout response: " + obj);
      }
    }
  }

  private class MessageTask extends BasicTask {
    @Override
    protected JSONObject doInBackground(String... params) {
      try {
        HashMap<String, String> toPost = new HashMap<String, String>(5);
        toPost.put(FIELD_TASK, TASK_MESSAGE);
        toPost.put(FIELD_TOKEN, mToken);
        JSONObject msgs;
        try {
        	synchronized (mMessageQueue) {
  					msgs = new JSONObject().put(FIELD_MESSAGES, new JSONArray(mMessageQueue));
  					mMessageQueue.clear();
					}
				} catch (JSONException e) {
					Log.e(TAG, "Message JSON error", e);
					msgs = new JSONObject();
				}
				String messages = msgs.toString();
        toPost.put(FIELD_MESSAGES, messages);
        Log.d(TAG, "send: " + messages);
        
        return sendRequest(toPost);
      } catch (GameException e) {
        Log.e(TAG, "Message Exception", e);
        try {
					return new JSONObject().put("error", e.getMessage());
				} catch (JSONException e1) {
          return null;
				}
      }
    }
    @Override
    protected void handleResult(JSONObject obj) {
    	// do nothing if the task was canceled during the request
      if (obj.has(FIELD_ERROR)) {
      	String error = obj.optString(FIELD_ERROR);
      	if (ERROR_INVALID_TOKEN.equals(error) || ERROR_USER_NOT_LOGGED.equals(error)) {
      		mToken = null; // remove token
      		setPolling(false); // also stop polling
      	}
    		if (mListener != null)
    			mListener.error(error);
      } else if (obj.has(FIELD_MESSAGES)) {
    		// message response
        Log.d(TAG, "receive: " + obj);
      	JSONArray msgs = obj.optJSONArray(FIELD_MESSAGES);
      	if (msgs != null) {
      		int len = msgs.length();
      		for (int i = 0; i < len; i++) {
      			JSONObject o = msgs.optJSONObject(i);
      			if (o != null)
      				processMessage(o);
      		}
      	}
      } else {
      	Log.e(TAG, "Invalid message response: " + obj);
      }
      if (mPolling && mHandler != null) {
      	mHandler.postDelayed(mSendMessagesRunnable, mPollTimeout);
      }
    }
    
    private void processMessage(JSONObject obj) {
    	if (ACTION_WAITING_FOR_OPPONENT.equals(obj.optString(FIELD_ACTION))) {
      	setPolling(true);
    		if (mListener != null)
    			mListener.waitingForOpponent();
    	} else if (ACTION_NEW_GAME.equals(obj.optString(FIELD_ACTION))) {
      	setPolling(true);
    		if (mListener != null)
    			mListener.newGame(obj.optString(FIELD_OPPONENT), obj.optInt(FIELD_SCORE), obj.optBoolean(FIELD_FIRST));
    	} else if (ACTION_YOUR_TURN.equals(obj.optString(FIELD_ACTION))) {
    		if (mListener != null)
    			mListener.yourTurn(obj.optInt(FIELD_X), obj.optInt(FIELD_Y));
    	} else if (ACTION_REMATCH.equals(obj.optString(FIELD_ACTION))) {
    		if (mListener != null)
    			mListener.rematch();
    	} else if (ACTION_OPPONENT_LEFT.equals(obj.optString(FIELD_ACTION))) {
      	setPolling(false);
    		if (mListener != null)
    			mListener.opponentLeft();
    	} else if (ACTION_USER_STATISTICS.equals(obj.optString(FIELD_ACTION))) {
    		if (mListener != null)
    			mListener.userStatistics(obj.optInt(FIELD_ONLINE_USERS), obj.optInt(FIELD_PLAYING_USERS));
      	if (mHandler != null)
      		mHandler.postDelayed(mStatisticsMessagesRunnable, mStatisticsTimeout);
    	} else if (ACTION_SEND_MESSAGE.equals(obj.optString(FIELD_ACTION))) {
    		if (mListener != null)
    			mListener.messageFrom(obj.optString(FIELD_OPPONENT), obj.optString(FIELD_MESSAGE_TEXT));
    	} else if (ACTION_YOU_WON.equals(obj.optString(FIELD_ACTION))) {
    		if (mListener != null) {
    			JSONArray scores = obj.optJSONArray(FIELD_SCORE);
    			mListener.youWon(scores.optInt(0), scores.optInt(1), getWinnerRow(obj.optJSONArray(FIELD_WINNER_ROW)));
      	}
    	} else if (ACTION_YOU_LOOSE.equals(obj.optString(FIELD_ACTION))) {
    		if (mListener != null) {
    			JSONArray scores = obj.optJSONArray(FIELD_SCORE);
    			mListener.youLoose(obj.optInt(FIELD_X), obj.optInt(FIELD_Y), scores.optInt(0), scores.optInt(1), getWinnerRow(obj.optJSONArray(FIELD_WINNER_ROW)));
    		}
    	} else if (ACTION_INVALID_MOVE.equals(obj.optString(FIELD_ACTION))) {
    		if (mListener != null)
    			mListener.error(getResourceString(R.string.errorInvalidMove));
    	} else if (ACTION_INVALID_STATE.equals(obj.optString(FIELD_ACTION))) {
    		if (mListener != null)
    			mListener.error(getResourceString(R.string.errorInvalidState));
    	} else if (ACTION_UPDATE_PROFILE.equals(obj.optString(FIELD_ACTION))) {
    		if (mListener != null)
    			mListener.profileUpdated();
    	}
    }
    
		private int[][] getWinnerRow(JSONArray win) {
			int l = win.length();
			int[][] winnerRow = new int[l][2];
			for (int i = 0; i < l; i++) {
				JSONArray e = win.optJSONArray(i);
				winnerRow[i][0] = e.optInt(0);
				winnerRow[i][1] = e.optInt(1);
			}
			return winnerRow;
		}
  }


  private class RegisterTask extends BasicTask {
    private String mUsername;
    private String mPassword;
    @Override
    protected JSONObject doInBackground(String... params) {
      try {
        HashMap<String, String> toPost = new HashMap<String, String>(5);
        toPost.put(FIELD_TASK, TASK_REGISTER);
        mUsername = params[0];
        mPassword = params[1];
        toPost.put(FIELD_USERNAME, mUsername);
        toPost.put(FIELD_PASSWORD, mPassword);
        toPost.put(FIELD_GAMEID, GAMEID);
        toPost.put(FIELD_DETAILS, params[2]);
        return sendRequest(toPost);
      } catch (GameException e) {
        Log.e(TAG, "Register Exception", e);
        try {
					return new JSONObject().put("error", e.getMessage());
				} catch (JSONException e1) {
          return null;
				}
      }
    }
    @Override
    protected void handleResult(JSONObject obj) {
    	// do nothing if the task was canceled during the request
      if (obj.has(FIELD_ERROR)) {
    		if (mListener != null)
    			mListener.error(obj.optString(FIELD_ERROR));
      } else if (STATUS_OK.equals(obj.optString(FIELD_STATUS)) && TASK_REGISTER.equals(obj.optString(FIELD_TASK))) {
    		// register success
    		if (mListener != null)
    			mListener.registered(mUsername, mPassword);
      } else {
      	Log.e(TAG, "Invalid register response: " + obj);
      }
    }
  }


	@Override
	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
		if (PREF_KEY_SERVICE_URL.equals(key)) {
			mServerURL = sharedPreferences.getString(key, null);
			Log.i(TAG, "New serviceURL: " + mServerURL);
		}
	}
}
